package com.xucia.jsponic.remote;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Logger;

import javax.security.auth.login.LoginException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ScriptRuntime;

import com.xucia.jsponic.data.DataSourceManager;
import com.xucia.jsponic.data.Identification;
import com.xucia.jsponic.data.ObjectPath;
import com.xucia.jsponic.data.Persistable;
import com.xucia.jsponic.datasource.LocalJsponFileSource;
import com.xucia.jsponic.job.InstallStarterFiles;
import com.xucia.jsponic.job.Job;
import com.xucia.jsponic.job.OrphanPurger;
import com.xucia.jsponic.job.Upgrade;
import com.xucia.jsponic.remote.ClientConnection.PostResponse;
import com.xucia.jsponic.security.ObjectAccessDeniedException;
import com.xucia.jsponic.security.User;
import com.xucia.jsponic.security.UserSecurity;

@SuppressWarnings("serial")
public class StandardJsponFilter extends JsponicServlet implements Filter {
	public void init(FilterConfig config) throws ServletException {
		String webappRoot = config.getServletContext().getRealPath("");
		LocalJsponFileSource.setLocalJsponPath(webappRoot);
		InstallStarterFiles installStarterFiles = new InstallStarterFiles();
		installStarterFiles.execute(); // TODO: Part of the starter files needs to execute for each context, part just one (the WEB-INF shared stuff)
		Job upgrade = new Upgrade();
		upgrade.execute();
		OrphanPurger orphanPurger = new OrphanPurger();
		orphanPurger.execute();
	}
	public static interface RequestListener {
		public void request(HttpServletRequest request);
	}
	static Set<RequestListener> requestListeners = new HashSet<RequestListener>();
	public static void addRequestListener(RequestListener listener) {
		requestListeners.add(listener);
	}
	private void addHeaders(Object headerTarget) throws JSONException {
		JSONObject accessLevel = ClientConnection.getCurrentObjectResponse()
				.getAccessLevels();
		accessLevel.put("username", User.currentUser().getName());
		//System.err.println("access Level " + pathInfo + " " + accessLevel.toString());
		setHeader("Access-Level", accessLevel.toString(),headerTarget);
	}

	public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
			final FilterChain filterChain) throws IOException, ServletException {
			HttpServletRequest request = (HttpServletRequest) servletRequest;
			HttpServletResponse response = (HttpServletResponse) servletResponse;
			String postBody = slurp(request.getInputStream());
	    	for (RequestListener listener : requestListeners)
	    		listener.request(request);
		final RequestHelper requestHelper = new RequestHelper((HttpServletRequest)request,(HttpServletResponse)response);
					String postResponseId = request.getParameter("PostResponse");
					long startTime = new Date().getTime();
					boolean doingRedirectedPost = postResponseId != null && request.getMethod().equalsIgnoreCase("POST"); 
			        Writer writer;
			        JSONObject suffixHeader = null;
			        PostResponse postResponse = null;
			        String suffixString = null;
			    	Object headerTarget;
			    	String callbackParameter;
					String pathInfo = URLDecoder.decode(request.getRequestURI().substring(request.getContextPath().length()+1),"UTF8");
					if (pathInfo.indexOf("dyna/dyna/dyna/dyna/") > -1)
						throw new RuntimeException("Excessively nested dynas from " + request.getRequestURL() + " browser " + request.getHeader("USER-AGENT") + " from " + request.getRemoteHost() + " referred by " + request.getHeader("Referer"));
					// System.err.println("Doing post response " +
					// postResponseId + " post part " + doingRedirectedPost);
					if (doingRedirectedPost) {
						response.setContentType("text/html");
						postResponse = ClientConnection.getCurrentObjectResponse().newPostResponse(postResponseId);
						writer = postResponse.writer;
						headerTarget = suffixHeader = postResponse.headers;
						Writer realWriter = new PrintWriter(response.getOutputStream(),true);
						realWriter.write("<html><body>Response will be in the subsequent get with PostResponse=" + postResponseId + "</body></html>");
						realWriter.close();
					}
					else if ((callbackParameter = request.getParameter("jsonp")) != null || (callbackParameter = request.getParameter("callback")) != null) { //TODO: Add callback parameter check here as well
						response.setContentType("text/javascript");
						headerTarget = suffixHeader = new JSONObject();
						writer = new PrintWriter(response.getOutputStream());
						if (pathInfo.endsWith(".js")) {
							writer.write("temp=");
							writer.flush();
							suffixString = "\n" + callbackParameter + "(temp";							
						}
						else
							writer.write(callbackParameter + "(");
			    	}
					else {
						response.setContentType("application/jsonrequest");
						writer = new PrintWriter(response.getOutputStream());
						headerTarget = response;
					}
			        // response.setCharacterEncoding("UTF16");
			        // System.err.println("parameters " + parameters);

			    	if (suffixHeader != null) {
			            DateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
			            formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
			            try {
				            suffixHeader.put("Date", formatter.format(new Date()));
						} catch (JSONException e) {
							throw new RuntimeException(e);
						}
			    	}
			    	try {
				    	if (postResponseId != null && !doingRedirectedPost) { // we are delivering the post response
				    		postResponse = ClientConnection.getCurrentObjectResponse().pullPostResponse(postResponseId);
				    		writer.write(postResponse.writer.toString());
				    		suffixHeader = postResponse.headers;
				    	}
				    	else {
							int slashIndex = pathInfo.indexOf('/');
							int dotIndex = pathInfo.indexOf('.');
							slashIndex = slashIndex == -1 ? dotIndex : dotIndex == -1 ? slashIndex : Math.min(slashIndex,pathInfo.indexOf('.'));
							Object source;
							//System.err.println("pathINfo " + pathInfo);
							if (slashIndex > -1) {
								String sourceName = pathInfo.substring(0,slashIndex);
								requestHelper.setPath(sourceName + '/');
								source = DataSourceManager.getSource(sourceName);
								if (source == null)
									source = AliasIds.getAliasHandler(sourceName);
							}
							else
								source = null;
							// TODO: all these should go in DataSources.json
							if ((source == null || source instanceof LocalDataSource) && AliasIds.getAliasHandler(pathInfo) == null 
									&& !"authenticate".equals(pathInfo)) {
								JsponicResponse redirectResponse = new JsponicResponse(response);
								filterChain.doFilter(new JsponicRequest(request,requestHelper, suffixString != null), redirectResponse);
								redirectResponse.getWriter().flush();
								return;
							}


		/*
		 * if ("GET".equals(request.getMethod())) // if it is a get and it is a local
		 * file if (staticFulfilment(request,requestHelper.getResponse(),writer))
		 * return;
		 */
				        	try {
								pathInfo = URLDecoder.decode(pathInfo,"UTF8");
					    		String method = request.getMethod().toUpperCase();
						    	String reason = null;
					    		if ("GET".equals(method)) {
									setCacheHeader(response, 2000); // need to do this to make GETs to refresh in IE
									if ("authenticate".equals(pathInfo))  {
										writer.write(JSONObject.quote(authenticate(requestHelper)));
										return;
									}
					    		}
						    	//postBody = slurp(request.getInputStream());
						    	Object target = ClientConnection.getCurrentObjectResponse().requestData(pathInfo);
						    	
						    	if ("POST".equals(method) || "DELETE".equals(method) || "PUT".equals(method)) {
						    		String seqIdString = request.getHeader("Seq-Id");
						    		if (seqIdString == null)
						    			seqIdString = request.getParameter("Seq-Id");
						    		long seqId = -1;
						    		if (seqIdString != null)
						    			seqId = Long.parseLong(seqIdString);
						    		ClientConnection.Sequence sequence = requestHelper.getClientConnection().getSequenceObject(seqId);
						    	
							    	synchronized(sequence) {
							    		int tries = 0;
							    		while (seqId != -1 && sequence.seqId != seqId && tries++ < 5) {
									    	System.err.println("seqId " + seqId);
							    			sequence.notify();
							    			if (sequence.isExecuting)
							    				sequence.wait(1000);
							    			else
							    				sequence.wait(100);
							    		}
							    		try {
							    			sequence.isExecuting = true;
								    		sequence.seqId = seqId == -1 ? -1 : seqId + 1;
								    		if ("POST".equals(method)) {
								    			JSONObject postObject = null;
								    			try {
								    				postObject = new JSONObject(postBody); // TODO: Shouldn't do this twice
								    			}catch(JSONException e) {} 
									    		
									    		if (postObject != null && postObject.has("id") && postObject.has("method") && postObject.has("params") && postObject.get("params") instanceof JSONArray) {
									    			requestHelper.handleRPC(target,postObject);
											    	writer.write(ClientConnection.getCurrentObjectResponse().getWaitingRPCs());
											    	return;
									    		}
								        		Logger.getLogger(StandardJsponFilter.class.toString()).info("postString " + postObject);
								        		Object appendingItem = requestHelper.convertParameterValueToObject(postBody);
								   				if (target instanceof List)
								   					((List) target).add(appendingItem);
								   				else {
									   				Context cx = Context.enter();
									   		        int length = ScriptRuntime.toInt32(ScriptRuntime.getObjectProp(target, "length", cx));
									   		        ScriptRuntime.setObjectIndex(target, length, appendingItem, cx);
									   		        ScriptRuntime.setObjectProp(target, "length", ScriptRuntime.wrapNumber(length+1), cx);
								   				}
								   				target = appendingItem;
								    		}
								    		else if ("PUT".equals(method)) {
								    			//TODO: Need to pass the id in, the URL-id should take precedence over what is in the body of the JSON
								        		Logger.getLogger(StandardJsponFilter.class.toString()).info("putString " + postBody);
								        		Identification<? extends Object> id = Identification.idForString(pathInfo);
								        		target = requestHelper.convertWithKnownId(postBody,id);
								        		if (id instanceof ObjectPath) {
								        			Object key = ((ObjectPath)id).getLastPath();
								        			Persistable secondToLastTarget = (Persistable) ((ObjectPath)id).getSecondToLastTarget();
								        			if (key instanceof Integer)
								        				secondToLastTarget.put((Integer) key, secondToLastTarget, target);
								        			else
								        				secondToLastTarget.set((String) key, target);
								        		}
								        		
								    		}
								    		else if ("DELETE".equals(method)) {
								    			for (int i = ((List) target).size(); i > 0;) {
								    				i--;
								    				((List) target).remove(i);
								    			}
								    		}
							    		}
							    		finally {
							    			sequence.isExecuting = false;
							    		}
							    	}
						    	}
								String output = requestHelper.outputReturnObject(target);
								addHeaders(headerTarget);
								writer.write(output);
				            }
				            catch (Throwable e) {
				                e.printStackTrace();
				                while (e.getCause() != null)
				                    e = e.getCause();            
				                String reason = e.getMessage();
					            if (e instanceof SecurityException)
					            	response.setStatus(401);
					            if (e instanceof ObjectAccessDeniedException)
					            	response.setStatus(401);
					            else
					            	response.setStatus(500);
					            if (reason == null)
					            	writer.write("Failed");
					            else
					            	writer.write(reason);
				            }

				    	}
			    	}
			    	catch (Throwable e) {
			    		response.setStatus(500);
			            e.printStackTrace();
			            while (e.getCause() != null && e.getCause() != e)
			            	e = e.getCause();
			            String reason = e.getMessage();
			            if (reason == null)
			            	writer.write(e.getClass().toString());
			            else
			            	writer.write(reason); 
			    	}
			    	finally {
					    if (suffixString != null)
					    	writer.write(suffixString);
				    	if (suffixHeader != null && !doingRedirectedPost)
				    		writer.write("," + suffixHeader + ");");
				    	writer.flush();
						writer.close();
				    	//response.flushBuffer();
						if (doingRedirectedPost)
							postResponse.finished = true;
			    	}
				}

	public void setHeader(String name, String value, Object headerTarget) {
		if (headerTarget instanceof HttpServletResponse)
			((HttpServletResponse) headerTarget).setHeader(name, value);
		else if (headerTarget instanceof JSONObject) {
			try {
				((JSONObject) headerTarget).put(name, value);
			} catch (JSONException e) {
				throw new RuntimeException(e);
			}
		} else
			throw new RuntimeException("Unacceptable header target");
	}



	private String authenticate(RequestHelper requestHelper) {
		try {
			Map<String, Object> parameters = requestHelper
					.getParametersAsObjects();
			String username = (String) parameters.get("username");
			String password = (String) parameters.get("password");

			String age = (String) parameters.get("validFor");
			User user = UserSecurity.authenticate(username, password);
			String ticket = username + ":" + user.getCurrentTicket();
			/*
			 * Cookie cookie = new Cookie("Authorization",ticket); if (age ==
			 * null) cookie.setMaxAge(1000); else
			 * cookie.setMaxAge(Integer.parseInt(age)); /* String cookiePath =
			 * request.getContextPath(); if (cookiePath.length() == 0)
			 * cookiePath = "/"; cookie.setPath("/");
			 * requestHelper.getResponse().addCookie(cookie);
			 */
			ClientConnection.getCurrentObjectResponse().setLogin(
					User.currentUser());
			return ticket;
		} catch (LoginException e) {
			requestHelper.getResponse().setStatus(401);
			// requestHelper.getResponse().setHeader("WWW-Authenticate","");
			// //TODO: should set the correct challenge
			return "";
		}
	}
	private static void setCacheHeader(HttpServletResponse response, long expirationTime) {
		response.setHeader("Pragma","");
        response.setHeader("Cache-Control","public");
        Date expirationDate = new Date(new Date().getTime() + expirationTime);
        DateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz",Locale.US);
        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
        response.setHeader("Expires",formatter.format(expirationDate));
	}

	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		setCacheHeader(response, 2000);
		RequestHelper requestHelper = new RequestHelper(request, response);
		doPost(requestHelper);
	}
	public static interface LocalDataSource {
		
	}
    public static String slurp (InputStream in) throws IOException {
        StringBuffer out = new StringBuffer();
        byte[] b = new byte[4096];
        for (int n; (n = in.read(b)) != -1;) {
            out.append(new String(b, 0, n));
        }
        return out.toString();
    }

}